/**
 *  @file
 *  @copyright defined in eos/LICENSE.txt
 */
#include <utility>
#include <vector>
#include <string>
#include <eosiolib/eosio.hpp> //eosio核心库
#include <eosiolib/time.hpp>  //eosio时间管理库
#include <eosiolib/asset.hpp>
#include <eosiolib/contract.hpp>
#include <eosiolib/crypto.h>

using eosio::key256;
using eosio::indexed_by;
using eosio::const_mem_fun;
using eosio::asset;
using eosio::permission_level;
using eosio::action;
using eosio::print;
using eosio::name;

class dice : public eosio::contract { //继承contract基类
  private:
    //@abi table offer i64
    struct offer                //定义数据库表 offer
    {
        uint64_t id;            //uint64_t通常用于定义ID
        account_name owner;     //账户数据类型 account_name，参考 https://developers.eos.io/eosio-cpp/reference#account_name
        asset bet;              //资产数据类型，参考 https://developers.eos.io/eosio-cpp/reference#asset
        checksum256 commitment; //
        uint64_t gameid = 0;    //游戏ID

        uint64_t primary_key() const { return id; } //定义主键

        uint64_t by_bet() const { return (uint64_t)bet.amount; }  //定义下注金额，asset数据类型的属性 amount

        key256 by_commitment() const { return get_commitment(commitment); }

        static key256 get_commitment(const checksum256 &commitment)
        {
            const uint64_t *p64 = reinterpret_cast<const uint64_t *>(&commitment);
            return key256::make_from_word_sequence<uint64_t>(p64[0], p64[1], p64[2], p64[3]);
        }

        EOSLIB_SERIALIZE(offer, (id)(owner)(bet)(commitment)(gameid))
    };

    typedef eosio::multi_index<N(offer), offer,
                               indexed_by<N(bet), const_mem_fun<offer, uint64_t, &offer::by_bet>>,
                               indexed_by<N(commitment), const_mem_fun<offer, key256, &offer::by_commitment>>>
        offer_index; //定义一个多索引数据类型 offer_index，该实例以id为主键，bet.amount和commitment为索引键

    struct player //玩家数据结构，这个不用做成数据表，不需要 @abi table
    {
        checksum256 commitment;       //玩家的SHA256
        checksum256 reveal;           //玩家的随机密钥，这个密钥用来生成上面的SHA256

        EOSLIB_SERIALIZE(player, (commitment)(reveal))
    };

    //@abi table game i64
    struct game
    {
        uint64_t id;                         //游戏ID
        asset bet;                           //下注树
        eosio::time_point_sec deadline;      //到期时间,加5分钟
        player player1;                      //玩家1
        player player2;                      //玩家2

        uint64_t primary_key() const { return id; }  //设置主键

        EOSLIB_SERIALIZE(game, (id)(bet)(deadline)(player1)(player2))
    };

    typedef eosio::multi_index<N(game), game> game_index; //定义一个game类型：game_index

    //@abi table global i64
    struct global_dice
    {
        uint64_t id = 0;
        uint64_t nextgameid = 0;

        uint64_t primary_key() const { return id; }

        EOSLIB_SERIALIZE(global_dice, (id)(nextgameid))
    };

    typedef eosio::multi_index<N(global), global_dice> global_dice_index;

    //@abi table account i64
    struct account //参与账户类
    {
        account(account_name o = account_name()) : owner(o) {}

        account_name owner;          //账户名
        asset eos_balance;           //充值的代币余额
        uint32_t open_offers = 0;    //尚未下注的游戏
        uint32_t open_games = 0;     //尚未结束的游戏

        bool is_empty() const { return !(eos_balance.amount | open_offers | open_games); } //如果充值额为0，或者没有尚未下注的游戏，或者没有尚未结束的游戏，则说明是空

        uint64_t primary_key() const { return owner; } //用户名为主键，不能同时进行两个游戏

        EOSLIB_SERIALIZE(account, (owner)(eos_balance)(open_offers)(open_games))
    };

    typedef eosio::multi_index<N(account), account> account_index;  //定义一个账户类型：account_index

    //实例化数据表
    offer_index offers;
    game_index games;
    global_dice_index global_dices;
    account_index accounts;

    //内部函数
    bool has_offer(const checksum256 &commitment) const
    {
        //是否已经下注
        auto idx = offers.template get_index<N(commitment)>(); //在多索引数据表中查找非主键数据的方法
        auto itr = idx.find(offer::get_commitment(commitment));
        return itr != idx.end();
    }

    bool is_equal(const checksum256 &a, const checksum256 &b) const
    {
        return memcmp((void *)&a, (const void *)&b, sizeof(checksum256)) == 0;
    }

    bool is_zero(const checksum256 &a) const
    {
        const uint64_t *p64 = reinterpret_cast<const uint64_t *>(&a);
        return p64[0] == 0 && p64[1] == 0 && p64[2] == 0 && p64[3] == 0;
    }

    void pay_and_clean(const game &g, const offer &winner_offer,
                       const offer &loser_offer)
    {

        // Update winner account balance and game count
        auto winner_account = accounts.find(winner_offer.owner);
        accounts.modify(winner_account, 0, [&](auto &acnt) {
            acnt.eos_balance += 2 * g.bet;
            acnt.open_games--;
        });

        // Update losser account game count
        auto loser_account = accounts.find(loser_offer.owner);
        accounts.modify(loser_account, 0, [&](auto &acnt) {
            acnt.open_games--;
        });

        if (loser_account->is_empty())
        {
            accounts.erase(loser_account);
        }

        games.erase(g);
        offers.erase(winner_offer);
        offers.erase(loser_offer);
    }

  public:
    const uint32_t FIVE_MINUTES = 5 * 60;

    dice(account_name self)
        : eosio::contract(self),
          offers(_self, _self),
          games(_self, _self),
          global_dices(_self, _self),
          accounts(_self, _self)
    {}

    //@abi action
    void deposit(const account_name from, const asset &quantity)
    {
        //充值
        eosio_assert(quantity.is_valid(), "invalid quantity");                  //验证资产数据格式是否合法
        eosio_assert(quantity.amount > 0, "must deposit positive quantity");

        //操作accounts数据表，如果没有，则添加新用户
        auto itr = accounts.find(from);
        if (itr == accounts.end())
        {
            itr = accounts.emplace(_self, [&](auto &acnt) {
                acnt.owner = from;
            });
        }

        //转账动作组合
        //相当于：cleos push action eosio.token transfer '[from, _self, quantity]' -p from@active
        //_self是合约账户
        action(
            permission_level{from, N(active)},
            N(eosio.token), N(transfer),
            std::make_tuple(from, _self, quantity, std::string("")))
            .send();

        //如果转账成功，则账户余额添加
        accounts.modify(itr, 0, [&](auto &acnt) {
            acnt.eos_balance += quantity;
        });
    }

    //@abi action
    void withdraw(const account_name to, const asset &quantity)
    {
        require_auth(to);

        eosio_assert(quantity.is_valid(), "invalid quantity");
        eosio_assert(quantity.amount > 0, "must withdraw positive quantity");

        auto itr = accounts.find(to);
        eosio_assert(itr != accounts.end(), "unknown account");

        accounts.modify(itr, 0, [&](auto &acnt) {
            eosio_assert(acnt.eos_balance >= quantity, "insufficient balance");
            acnt.eos_balance -= quantity;
        });

        //提现动作
        //相当于：cleos push action eosio.token transfer '[_self, from, quantity]' -p _self@active
        action(
            permission_level{_self, N(active)}, //BUG：应该改为 permission_level{_self, N(eosio.code)}
            N(eosio.token), N(transfer),
            std::make_tuple(_self, to, quantity, std::string("")))
            .send();

        if (itr->is_empty())
        {
            accounts.erase(itr);
        }
    }

    //@abi action
    void offerbet(const asset &bet, const account_name player, const checksum256 &commitment)
    {
        //下注
        eosio_assert(bet.symbol == CORE_SYMBOL, "only core token allowed"); //eosio_assert 参考 https://developers.eos.io/eosio-cpp/reference#eosio_assert
        eosio_assert(bet.is_valid(), "invalid bet");
        eosio_assert(bet.amount > 0, "must bet positive quantity");

        eosio_assert(!has_offer(commitment), "offer with this commitment already exist"); //如果下注已经存在，则退出
        require_auth(player);                                                             //验证账户是否合法，参考 https://developers.eos.io/eosio-cpp/reference#require_auth

        auto cur_player_itr = accounts.find(player);                       //查找用户，accounts是账户数据表
        eosio_assert(cur_player_itr != accounts.end(), "unknown account"); //判断用户是否存在

        // Store new offer
        auto new_offer_itr = offers.emplace(_self, [&](auto &offer) {           //在下注的数据表中，用emplace添加数据
            offer.id         = offers.available_primary_key();                  //使用自增型主键
            offer.bet        = bet;
            offer.owner      = player;
            offer.commitment = commitment;
            offer.gameid     = 0;
        });

        // Try to find a matching bet
        // 寻找是否相同金额的下注方
        auto idx = offers.template get_index<N(bet)>();
        auto matched_offer_itr = idx.lower_bound((uint64_t)new_offer_itr->bet.amount);

        if (matched_offer_itr == idx.end() || matched_offer_itr->bet != new_offer_itr->bet || matched_offer_itr->owner == new_offer_itr->owner)
        {

            // No matching bet found, update player's account
            accounts.modify( cur_player_itr, 0, [&](auto& acnt) {
               eosio_assert( acnt.eos_balance >= bet, "insufficient balance" );//充值额不够
               acnt.eos_balance -= bet; //扣除余额
               acnt.open_offers++;      //下注次数+1
            });

         } else {
            // Create global game counter if not exists
            auto gdice_itr = global_dices.begin();
            if( gdice_itr == global_dices.end() ) {
               gdice_itr = global_dices.emplace(_self, [&](auto& gdice){
                  gdice.nextgameid=0;
               });
            }

            // Increment global game counter
            global_dices.modify(gdice_itr, 0, [&](auto& gdice){
               gdice.nextgameid++;
            });

            // Create a new game
            auto game_itr = games.emplace(_self, [&](auto& new_game){
               new_game.id       = gdice_itr->nextgameid;
               new_game.bet      = new_offer_itr->bet;
               new_game.deadline = eosio::time_point_sec(0);

               new_game.player1.commitment = matched_offer_itr->commitment;
               memset(&new_game.player1.reveal, 0, sizeof(checksum256));

               new_game.player2.commitment = new_offer_itr->commitment;
               memset(&new_game.player2.reveal, 0, sizeof(checksum256));
            });

            // Update player's offers
            idx.modify(matched_offer_itr, 0, [&](auto& offer){
               offer.bet.amount = 0;
               offer.gameid = game_itr->id;
            });

            offers.modify(new_offer_itr, 0, [&](auto& offer){
               offer.bet.amount = 0;
               offer.gameid = game_itr->id;
            });

            // Update player's accounts
            accounts.modify( accounts.find( matched_offer_itr->owner ), 0, [&](auto& acnt) {
               acnt.open_offers--;
               acnt.open_games++;
            });

            accounts.modify( cur_player_itr, 0, [&](auto& acnt) {
               eosio_assert( acnt.eos_balance >= bet, "insufficient balance" );
               acnt.eos_balance -= bet;
               acnt.open_games++;
            });
         }
      }

      //@abi action
      void canceloffer( const checksum256& commitment ) {

         auto idx = offers.template get_index<N(commitment)>();
         auto offer_itr = idx.find( offer::get_commitment(commitment) );

         eosio_assert( offer_itr != idx.end(), "offer does not exists" );
         eosio_assert( offer_itr->gameid == 0, "unable to cancel offer" );
         require_auth( offer_itr->owner );

         auto acnt_itr = accounts.find(offer_itr->owner);
         accounts.modify(acnt_itr, 0, [&](auto& acnt){
            acnt.open_offers--;
            acnt.eos_balance += offer_itr->bet;
         });

         idx.erase(offer_itr);
      }

      //@abi action
      void reveal( const checksum256& commitment, const checksum256& source ) {
         //开奖 
         assert_sha256( (char *)&source, sizeof(source), (const checksum256 *)&commitment );

         //获取当前执行reveal的人的SHA256
         auto idx = offers.template get_index<N(commitment)>();
         auto curr_revealer_offer = idx.find( offer::get_commitment(commitment)  );

         eosio_assert(curr_revealer_offer != idx.end(), "offer not found");
         eosio_assert(curr_revealer_offer->gameid > 0, "unable to reveal"); //如果已经开过

         auto game_itr = games.find( curr_revealer_offer->gameid ); //查询对应的game

         player curr_reveal = game_itr->player1; //获取玩家1
         player prev_reveal = game_itr->player2; //获取玩家2

         if( !is_equal(curr_reveal.commitment, commitment) ) {
            std::swap(curr_reveal, prev_reveal);
         }

         eosio_assert( is_zero(curr_reveal.reveal) == true, "player already revealed");//判断玩家是否已经开奖

         if( !is_zero(prev_reveal.reveal) ) {
            //比较双方随机数的大小
            checksum256 result;
            sha256( (char *)&game_itr->player1, sizeof(player)*2, &result);

            auto prev_revealer_offer = idx.find( offer::get_commitment(prev_reveal.commitment) );

            int winner = result.hash[1] < result.hash[0] ? 0 : 1;

            if( winner ) {
               pay_and_clean(*game_itr, *curr_revealer_offer, *prev_revealer_offer);
            } else {
               pay_and_clean(*game_itr, *prev_revealer_offer, *curr_revealer_offer);
            }

         } else {
            games.modify(game_itr, 0, [&](auto& game){

               if( is_equal(curr_reveal.commitment, game.player1.commitment) )
                  game.player1.reveal = source;
               else
                  game.player2.reveal = source;

               game.deadline = eosio::time_point_sec(now() + FIVE_MINUTES);
            });
         }
      }

      //@abi action
      void claimexpired( const uint64_t gameid ) {

         auto game_itr = games.find(gameid);

         eosio_assert(game_itr != games.end(), "game not found");
         eosio_assert(game_itr->deadline != eosio::time_point_sec(0) && eosio::time_point_sec(now()) > game_itr->deadline, "game not expired");

         auto idx = offers.template get_index<N(commitment)>();
         auto player1_offer = idx.find( offer::get_commitment(game_itr->player1.commitment) );
         auto player2_offer = idx.find( offer::get_commitment(game_itr->player2.commitment) );

         if( !is_zero(game_itr->player1.reveal) ) {
            eosio_assert( is_zero(game_itr->player2.reveal), "game error");
            pay_and_clean(*game_itr, *player1_offer, *player2_offer);
         } else {
            eosio_assert( is_zero(game_itr->player1.reveal), "game error");
            pay_and_clean(*game_itr, *player2_offer, *player1_offer);
         }

      }
};

EOSIO_ABI( dice, (offerbet)(canceloffer)(reveal)(claimexpired)(deposit)(withdraw) )

